package perf;

import editor.PwnIDE;
import util.ArrayUtil;

import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.text.DecimalFormat;

/**
 * @author Neil Dickson
 */
public class PerformanceViewer extends JDialog {
	private int numDataSets;
	private double nMax;
	private double nMin;
	private double nScale;
	private double tMax;
	private double tMin;
	private double tScale;
	private final double[][] data;
	private int[] scalings;
	private double[][] coefficients;
	private String[] names;
	private Color[] colours;

	public static final int SCALING_1		= 0;
	public static final int SCALING_LOGN	= 1;
	public static final int SCALING_N		= 2;
	public static final int SCALING_NLOGN	= 3;
	public static final int SCALING_N2		= 4;

	public static final DecimalFormat	COEFFICIENT_FORMAT = new DecimalFormat("0.0");
	public static final DecimalFormat	COEFFICIENT_FORMAT2 = new DecimalFormat(" + 0.0; - 0.0");

	public static final ScalingFunction[] SCALING_FUNCTIONS = {
			new ScalingFunction() {
				public int numFunctions() {
					return 1;
				}
				public void eval(double n, double[] fOfn) {
					fOfn[0] = 1;
				}
				public double evalFull(double n,double[] coefficients) {
					return coefficients[0];
				}
				public String toString(double[] coefficients) {
					return COEFFICIENT_FORMAT.format(coefficients[0]);
				}
			},
			new ScalingFunction() {
				public int numFunctions() {
					return 2;
				}
				public void eval(double n, double[] fOfn) {
					fOfn[0] = 1;
					fOfn[1] = Math.log(n)/Math.log(2);
				}
				public double evalFull(double n,double[] coefficients) {
					return coefficients[0] + (Math.log(n)/Math.log(2))*coefficients[1];
				}
				public String toString(double[] coefficients) {
					return COEFFICIENT_FORMAT.format(coefficients[1])+"logn"+COEFFICIENT_FORMAT2.format(coefficients[0]);
				}
			},
			new ScalingFunction() {
				public int numFunctions() {
					return 2;
				}
				public void eval(double n, double[] fOfn) {
					fOfn[0] = 1;
					fOfn[1] = n;
				}
				public double evalFull(double n,double[] coefficients) {
					return coefficients[0] + n*coefficients[1];
				}
				public String toString(double[] coefficients) {
					return COEFFICIENT_FORMAT.format(coefficients[1])+"n"+COEFFICIENT_FORMAT2.format(coefficients[0]);
				}
			},
			new ScalingFunction() {
				public int numFunctions() {
					return 3;
				}
				public void eval(double n, double[] fOfn) {
					fOfn[0] = 1;
					fOfn[1] = n;
					fOfn[2] = n*Math.log(n)/Math.log(2);
				}
				public double evalFull(double n,double[] coefficients) {
					return coefficients[0] + n*coefficients[1] + n*(Math.log(n)/Math.log(2))*coefficients[2];
				}
				public String toString(double[] coefficients) {
					return COEFFICIENT_FORMAT.format(coefficients[2])+"nlogn"+COEFFICIENT_FORMAT2.format(coefficients[1])+"n"+COEFFICIENT_FORMAT2.format(coefficients[0]);
				}
			},
			new ScalingFunction() {
				public int numFunctions() {
					return 3;
				}
				public void eval(double n, double[] fOfn) {
					fOfn[0] = 1;
					fOfn[1] = n;
					fOfn[2] = n*n;
				}
				public double evalFull(double n,double[] coefficients) {
					return coefficients[0] + n*coefficients[1] + n*n*coefficients[2];
				}
				public String toString(double[] coefficients) {
					return COEFFICIENT_FORMAT.format(coefficients[2])+"n^2"+COEFFICIENT_FORMAT2.format(coefficients[1])+"n"+COEFFICIENT_FORMAT2.format(coefficients[0]);
				}
			}
	};

	public static final double[] NICE_UPPER_BOUNDS = {0.12, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.75, 1.0};
	public static final double[] NICE_SCALES = {0.01, 0.01, 0.02, 0.025, 0.025, 0.04, 0.05, 0.05, 0.05, 0.1};
	public static final BasicStroke BEST_FIT_STROKE = new BasicStroke(2.0f);

	public static final Color[] COLOURS = {new Color(0x1038A0),new Color(0xA81818),new Color(0x207008),new Color(0xC07000),new Color(0x501880)};

	public PerformanceViewer(String groupName, File[] files, double[][] data) {
		super(PwnIDE.editor,groupName+" Performance Analysis");
		setSize(800,600);
		setLayout(new BorderLayout());
		numDataSets = data.length;
		this.data = data;
		scalings = new int[numDataSets];
		names = new String[numDataSets];
		colours = new Color[numDataSets];
		for (int i=0; i<numDataSets; ++i) {
			colours[i] = COLOURS[i%COLOURS.length];
			String filename = files[i].getName();
			int timeIndex = filename.lastIndexOf('_')+1;
			int dateIndex = filename.lastIndexOf('_',timeIndex-2)+1;
			int scalingIndex = filename.lastIndexOf('_',dateIndex-2)+1;
			String scalingText = filename.substring(scalingIndex,dateIndex-1);
			scalings[i] = ArrayUtil.indexOf(PerformanceTestInfo.BRIEF_SCALINGS,scalingText);
			if (scalings[i]==-1) {
				scalings[i] = SCALING_N;
			}
			int functionIndex = filename.lastIndexOf('_',scalingIndex-2)+1;
			String dateText = filename.substring(dateIndex,timeIndex-1)+" "+filename.substring(timeIndex,filename.length()-5);
			if (functionIndex==0) {
				// If there is no group name given, use the date/time as the identifier
				names[i] = dateText;
			}
			else {
				// If a group name is given, use the function name as its identifier
				names[i] = filename.substring(functionIndex,scalingIndex-1)+" "+dateText;
			}
		}

		eliminationLeastSquares();

		JPanel panel = new JPanel(null) {
			public void paintComponent(Graphics g) {
				int width = getWidth();
				int height = getHeight();
				FontMetrics font = getFontMetrics(getFont());
				g.setColor(Color.WHITE);
				g.fillRect(0,0,width,height);
				int xMin = 64;
				int xMax = width-192;
				int yMin = 32;
				int yMax = height-32;
				// Draw the grid (handle error in addition by allowing to go a bit past the end)
				g.setColor(Color.LIGHT_GRAY);
				for (double n = nMin+nScale; n < nMax+(nScale*0.01); n += nScale) {
					int x = xFromn(n,nMin,nMax,xMin,xMax);
					g.drawLine(x,yMin,x,yMax-1);
				}
				for (double t = tMin+tScale; t < tMax+(tScale*0.01); t += tScale) {
					int y = yFromt(t,tMin,tMax,yMin,yMax);
					g.drawLine(xMin+1,y,xMax,y);
				}
				// Draw the data points
				for (int dataSetIndex=0;dataSetIndex<numDataSets;++dataSetIndex) {
					double[] dataSet = PerformanceViewer.this.data[dataSetIndex];
					if (dataSet!=null) {
						g.setColor(colours[dataSetIndex]);
						for (int i=0;i<dataSet.length;i+=2) {
							double n = dataSet[i];
							double t = dataSet[i+1];
							if (!Double.isNaN(n)) {
								int x = xFromn(n,nMin,nMax,xMin,xMax);
								int y = yFromt(t,tMin,tMax,yMin,yMax);
								g.drawLine(x-1,y,x+1,y);
								g.drawLine(x,y-1,x,y+1);
							}
						}
					}
				}
				// Draw the curves of best fit
				Graphics2D g2 = (Graphics2D) g;
				g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
				g2.setStroke(BEST_FIT_STROKE);
				for (int dataSetIndex=0;dataSetIndex<numDataSets;++dataSetIndex) {
					if (PerformanceViewer.this.data[dataSetIndex]!=null) {
						g.setColor(colours[dataSetIndex]);
						ScalingFunction function = SCALING_FUNCTIONS[scalings[dataSetIndex]];
						int prevY = yFromt(function.evalFull(nFromx(xMin+1,nMin,nMax,xMin,xMax),coefficients[dataSetIndex]),tMin,tMax,yMin,yMax);
						for (int x=xMin+2;x<=xMax;++x) {
							int y = yFromt(function.evalFull(nFromx(x,nMin,nMax,xMin,xMax),coefficients[dataSetIndex]),tMin,tMax,yMin,yMax);
							if (y<yMax && prevY<yMax) {
								g.drawLine(x-1,prevY,x,y);
							}
							prevY = y;
						}
						if (prevY < 0) {
							prevY = 0;
						}
						else if (prevY>yMax) {
							prevY = yMax;
						}
						g.drawString(names[dataSetIndex],xMax,prevY+font.getHeight());
						g.drawString(function.toString(coefficients[dataSetIndex]),xMax,prevY+font.getHeight()*2);
					}
				}
				// Draw the ticks
				g.setColor(Color.BLACK);
				for (double n = nMin; n < nMax+(nScale*0.01); n += nScale) {
					int x = xFromn(n,nMin,nMax,xMin,xMax);
					g.drawLine(x,yMax,x,yMax+8);
					String label = (nScale>3) ? Integer.toString((int)n) : COEFFICIENT_FORMAT.format(n);
					g.drawString(label,x-(font.stringWidth(label)>>1),yMax+8+font.getHeight());
				}
				for (double t = tMin; t < tMax+(tScale*0.01); t += tScale) {
					int y = yFromt(t,tMin,tMax,yMin,yMax);
					g.drawLine(xMin-8,y,xMin,y);
					String label = (tScale>3) ? Integer.toString((int)t) : COEFFICIENT_FORMAT.format(t);
					g.drawString(label,xMin-8-font.stringWidth(label),y+(font.getHeight()>>1));
				}
				g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
				// Draw the border
				g.setColor(Color.BLACK);
				g.drawLine(xMin,yMin,xMin,yMax);
				g.drawLine(xMin,yMax,xMax,yMax);
			}
		};
		add(panel, BorderLayout.CENTER);
		updatePopupLocationMiddle();
	}

	private void eliminationLeastSquares() {
		nMin = 0;
		tMin = 0;
		nMax = 0.000000001;
		tMax = 0.000000001;
		coefficients = new double[numDataSets][];

		for (int dataSetIndex=0; dataSetIndex<numDataSets; ++dataSetIndex){
			double[] dataSet = data[dataSetIndex];
			int scaling = scalings[dataSetIndex];
			ScalingFunction function = SCALING_FUNCTIONS[scaling];
			int numFunctions = function.numFunctions();
			double[][] A = new double[numFunctions][numFunctions];
			double[] b = new double[numFunctions];
			double[] x = new double[numFunctions];
			double[] fOfn = new double[numFunctions];
			int numGoodPoints = 0;
			for (int i=0;i<dataSet.length;i+=2) {
				// Eliminate obvious bad points immediately
				if (dataSet[i]<0 || dataSet[i+1]<=0) {
					dataSet[i] = Double.NaN;
					dataSet[i+1] = Double.NaN;
				}
				else {
					double n = dataSet[i];
					double t = dataSet[i+1];
					function.eval(n,fOfn);
					boolean allValid = true;
					for (int j=0;j<numFunctions && allValid;++j) {
						allValid &= !Double.isNaN(fOfn[j]) && !Double.isInfinite(fOfn[j]) && fOfn[j]>0;
					}
					// Eliminate points with negative, zero, infinite, or NaN function values
					if (!allValid) {
						dataSet[i] = Double.NaN;
						dataSet[i+1] = Double.NaN;
					}
					else {
						++numGoodPoints;
						for (int j=0;j<numFunctions;++j) {
							b[j] += t*fOfn[j];
							for (int k=0;k<numFunctions;++k) {
								A[j][k] += fOfn[j]*fOfn[k];
							}
						}
					}
				}
			}
			// Remove data sets with no good points
			if (numGoodPoints==0) {
				data[dataSetIndex] = null;
			}
			else {
				for (int eliminationCount=0;eliminationCount<3;++eliminationCount) {
					solveLinearSystem(A,b,x);
					coefficients[dataSetIndex] = x;

					System.out.print(names[dataSetIndex]+": {");
					for (int j=0;j<numFunctions;++j) {
						System.out.print(coefficients[dataSetIndex][j]+",");
					}
					System.out.println("}");

					double mean = 0;
					double[] factors = new double[dataSet.length>>1];
					for (int i=0;i<dataSet.length;i+=2) {
						if (!Double.isNaN(dataSet[i])) {
							double expectedTime = function.evalFull(dataSet[i],coefficients[dataSetIndex]);
							factors[i>>1] = dataSet[i+1]/expectedTime;
							mean += factors[i>>1];
						}
					}
					mean /= numGoodPoints;
					System.out.println("Mean factor = "+mean);
					// NOTE: Using the standard deviation sucks, because the outliers make the standard deviation huge, and then they won't be eliminated.
					double averageAbsDeviation = 0;
					for (int i=0;i<dataSet.length;i+=2) {
						if (!Double.isNaN(dataSet[i])) {
							averageAbsDeviation += Math.sqrt(Math.abs(factors[i>>1]-mean));
						}
					}
					averageAbsDeviation = averageAbsDeviation/numGoodPoints;
					averageAbsDeviation *= averageAbsDeviation;
					System.out.println("Average abs deviation of factor = "+averageAbsDeviation);
					for (int i=0;i<dataSet.length;i+=2) {
						if (!Double.isNaN(dataSet[i])) {
							if (factors[i>>1]>mean+1.6*averageAbsDeviation) {
								function.eval(dataSet[i],fOfn);
								dataSet[i] = Double.NaN;
								dataSet[i+1] = Double.NaN;
								--numGoodPoints;
							}
						}
					}
					if (numGoodPoints==0) {
						data[dataSetIndex] = null;
						break;
					}
					// Recalculate the matrix and vector
					for (int j=0;j<numFunctions;++j) {
						b[j] = 0;
						for (int k=0;k<numFunctions;++k) {
							A[j][k] = 0;
						}
					}
					for (int i=0;i<dataSet.length;i+=2) {
						if (!Double.isNaN(dataSet[i])) {
							function.eval(dataSet[i],fOfn);
							for (int j=0;j<numFunctions;++j) {
								b[j] += dataSet[i+1]*fOfn[j];
								for (int k=0;k<numFunctions;++k) {
									A[j][k] += fOfn[j]*fOfn[k];
								}
							}
						}
					}
				}
				solveLinearSystem(A,b,x);
				coefficients[dataSetIndex] = x;
				for (int i=0;i<dataSet.length;i+=2) {
					if (!Double.isNaN(dataSet[i])) {
						nMax = Math.max(dataSet[i],nMax);
						tMax = Math.max(dataSet[i+1],tMax);
					}
				}
			}
		}
		double[] boundAndScale = new double[2];
		pickNiceUpperBoundAndScale(nMax,boundAndScale);
		nMax = boundAndScale[0];
		nScale = boundAndScale[1];
		pickNiceUpperBoundAndScale(tMax,boundAndScale);
		tMax = boundAndScale[0];
		tScale = boundAndScale[1];
	}


	public static int xFromn(double n,double nMin,double nMax,int xMin,int xMax) {
		return xMin + (int)((n-nMin)/(nMax-nMin)*(xMax-xMin));
	}

	public static double nFromx(int x,double nMin,double nMax,int xMin,int xMax) {
		return nMin + (x-xMin)*(nMax-nMin)/(xMax-xMin);
	}

	public static int yFromt(double t,double tMin,double tMax,int yMin,int yMax) {
		return yMin + (int)((tMax-t)/(tMax-tMin)*(yMax-yMin));
	}

	public static void pickNiceUpperBoundAndScale(double number,double[] upperBoundAndScale) {
		double power = Math.pow(10,Math.ceil(Math.log10(number)));
		double adjusted = number/power;
		int i=0;
		while (i<NICE_UPPER_BOUNDS.length-1 && adjusted>=NICE_UPPER_BOUNDS[i]) {
			++i;
		}
		upperBoundAndScale[0] = power*NICE_UPPER_BOUNDS[i];
		upperBoundAndScale[1] = power*NICE_SCALES[i];
	}

	public static void solveLinearSystem(double[][] a,double[] b,double[] x) {
		int n = b.length;
		System.arraycopy(b,0,x,0,n);
		// Gaussian elimination to get matrix into upper-triangular form
		for (int i=0; i<n-1; i++){
			for (int k=i+1; k<n; k++){
				double tempval = a[k][i]/a[i][i];
				for (int j=0; j<n; j++){
					a[k][j] -= a[i][j]*tempval;
				}
				x[k] -= x[i]*tempval;
			}
		}

		// Back-substitution to solve for variables x
		for (int i=n-1; i>=0; --i) {
			double sum = x[i];
			for (int j=i+1; j<n; ++j) {
				sum -= a[i][j]*x[j];
			}
			x[i] = sum/a[i][i];
		}
	}

	public void updatePopupLocationMiddle() {
		Point parentScreen = new Point(0,0);
		try {
			parentScreen = getOwner().getLocationOnScreen();
		}
		catch (Throwable t) {
			// Stuff gets thrown if the parent isn't visible yet
		}
		setLocation(new Point(((getOwner().getWidth()-getWidth())>>1)+parentScreen.x,((getOwner().getHeight()-getHeight())>>1) + parentScreen.y));
	}

	private abstract static class ScalingFunction {
		public abstract int numFunctions();
		public abstract void eval(double n,double[] fOfn);
		public abstract double evalFull(double n,double[] coefficients);
		public abstract String toString(double[] coefficients);
	}
}
